classdef dotsDrawableRDKgrid < dotsDrawable
    % @class dotsDrawableRDKgrid
    %  just a grid of dotsDrawableRDK objects
    
    properties (SetObservable = true)
        
        % the x-coordinate of the center of the grid (degrees visual
        % angle, centered)
        x = 0;
        
        % the y-coordinate of the center of the grid (degrees visual
        % angle, centered)
        y = 0;
        
        % grids, grids, grids
        gridSize = 3;
        
        % change in time, in ms
        timeInterval = 100;
        
        % the size of dots in the kinetogram (pixels)
        dotSize = 3;
        
        % one color for all dots in the field. (scalar clut index, [LA],
        % [RGB], or [RGBA], each value 0-255)
        color = [255 255 255];
        
        % the shape for all dots (integer, where 0 means filled square, 1
        % means filled circle, and 2 means filled circle with high-quality
        % anti-aliasing)
        shape = 0;

        % mean value of coherence in the grid
        coherenceMean = 50;
        
        % Range of coherence values (from unidrnd) in the grid
        coherenceRange  = 10;
        
        % density of dots in the kinetogram (dots per degree-visual-angle^2
        % per second)
        density = 16.7;
        
        % Mean direction of the motion signal, or an array of directions to
        % be picked at random by directionWeights (degrees,
        % counterclockwise, 0 = rightward)
        directionMean = 0;
        
        % range of directions from unifrnd
        directionRange  = 0;
        
        % when direction is an array, the relative frequency of each
        % direction (the pdf).  If directionWeights is incomplete, defaults
        % to equal weights.
        directionWeights = 1;
        
        % overall diameter of the full grid, (dva)
        diameter = 20;
        
        % fraction of diameter that determines the width of the field of
        % moving dots.  When fieldScale > 1, some dots will be hidden
        % behind the aperture.
        fieldScale = 1.1;
        
        % width of angular error to add to each dot's motion (degrees)
        drunkenWalk = 0;
        
        % speed of each dot's motion (degrees visual angle per second)
        speed = 3;
        
        % number disjoint sets of dots to interleave frame-by-frame
        interleaving = 3;
        
        % how to move coherent dots: as one rigid unit (true), or each dot
        % independently (false)
        isMovingAsHerd = false;
        
        % how to move non-coherent dots: by replotting from scratch (true),
        % or by local increments (false)
        isFlickering = true;
        
        % how to move dots near the edges: by wrapping to the other side
        % (true), or by replotting from scratch (false)
        isWrapping = true;
        
        % how to pick coherent dots: favoring recently non-coherent dots
        % (true), or indiscriminately (false)
        isLimitedLifetime = true;
                    
        % flag for circular aperture
        apertureFlag = true;
        
        % random number seed ... default to empty, which means do not seed
        %   'c' means seed with clock
        %   otherwise seed with given value
        seed = [];
        
        % random number stream.. putting here because might want to get it
        % from somewhere else
        randStream;
end
    
    properties (Hidden, SetObservable = false)

        % array of rdks in grid
        rdks;
        
        % time at start
        timeAtStart;
                        
        % Psychtoolbox Screen texture index for the dot field aperture mask
        maskTexture;
        
        % [x,y,x2,y2] rect, where to draw the dot field aperture mask,
        % (pixels, from the top-left of the window)
        maskDestinationRect;
        
        % [x,y,x2,y2] rect, spanning the entire dot field aperture mask,
        % (pixels, from the top-left of the window)
        maskSourceRect;

    end
    
    methods
        function self = dotsDrawableRDKgrid
            self            = self@dotsDrawable;
            self.color      = self.foregroundColor;
            self.randStream = RandStream.create('mt19937ar', 'seed', sum(100*clock));
       end
        
        function prepareToDrawInWindow(self)
            
            % initialize the random number stream
            if self.seed == 'c' % flag for clock
                reset(self.randStream,sum(100*clock));
            elseif ~isempty(self.seed)
                reset(self.randStream,self.seed);
            end            

            % center positions in grid
            ctrs = linspace( ...
                self.diameter.*(1-self.gridSize)./(2.*self.gridSize), ...
                self.diameter.*(self.gridSize-1)./(2.*self.gridSize), ...
                self.gridSize);
            
            % conditionally make grid if it doesn't exist, and fill it
            if size(self.rdks,1)==self.gridSize && size(self.rdks,2)==self.gridSize                
                reuse = true;                
            else
                delete(self.rdks); % cleanup if anything there)
                rdks(self.gridSize*self.gridSize) = dotsDrawableRDK;
                self.rdks = reshape(rdks, self.gridSize, self.gridSize);
                reuse = false;
            end
            
            for xx = 1:self.gridSize
                for yy = 1:self.gridSize
                    if reuse
                        rdk = self.rdks(xx,yy);
                    else
                        rdk = dotsDrawableRDK;
                    end
                    rdk.x                 = self.x+ctrs(xx);
                    rdk.y                 = self.y+ctrs(yy);
                    rdk.dotSize           = self.dotSize;
                    rdk.color             = self.color;
                    rdk.shape             = self.shape;
                    rdk.density           = self.density ;
                    rdk.diameter          = self.diameter/self.gridSize;
                    rdk.fieldScale        = 1.0;
                    rdk.drunkenWalk       = self.drunkenWalk;
                    rdk.speed             = self.speed;
                    rdk.interleaving      = self.interleaving;
                    rdk.isMovingAsHerd    = self.isMovingAsHerd;
                    rdk.isFlickering      = self.isFlickering;
                    rdk.isWrapping        = self.isWrapping;
                    rdk.isLimitedLifetime = self.isLimitedLifetime;
                    rdk.apertureFlag      = false;
                    rdk.seed              = [];
                    rdk.randStream        = self.randStream;
                    
                    rdk.coherence         = unidrnd(self.coherenceRange*2+1)-...
                        self.coherenceRange-1+self.coherenceMean;
                    rdk.direction         = unidrnd(self.directionRange*2+1)-...
                        self.directionRange-1+self.directionMean;
                    rdk.directionWeights  = self.directionWeights;
                    
                    rdk.prepareToDrawInWindow;
                    self.rdks(xx,yy) = rdk; 
                end
            end
            
            % initialize the random number stream
            if self.seed == 'c' % flag for clock
                self.randStream = RandStream.create('mt19937ar', 'seed', sum(clock));
            elseif ~isempty(self.seed)                
                self.randStream = RandStream.create('mt19937ar', 'seed', self.seed);
            end

            % build mask
            if self.apertureFlag
                
                fieldWidth               = self.diameter*self.fieldScale;
                fieldPixels              = ceil(fieldWidth * self.pixelsPerDegree);
                maskPixels               = fieldPixels + self.dotSize;
                self.maskSourceRect      = [0 0, maskPixels, maskPixels];
                
                pixelOrigin(1) = self.windowRect(3)/2 ...
                    + (self.x * self.pixelsPerDegree) - fieldPixels/2;
                pixelOrigin(2) = self.windowRect(4)/2 ...
                    - (self.y * self.pixelsPerDegree) - fieldPixels/2;
                self.maskDestinationRect = self.maskSourceRect ...
                    + pixelOrigin([1 2 1 2]) - self.dotSize/2;

                center      = exp(linspace(-1, 1, maskPixels).^2);
                field       = center'*center;
                if self.fieldScale > 1
                    marginWidth  = (self.fieldScale - 1) * self.diameter / 2;
                    marginPixels = ceil(marginWidth * self.pixelsPerDegree);
                    threshold    = center(marginPixels);
                else
                    threshold    = center(1);
                end
                aperture    = field > threshold;
                mask        = zeros(maskPixels, maskPixels, 4);
                mask(:,:,1) = self.backgroundColor(1);
                mask(:,:,2) = self.backgroundColor(2);
                mask(:,:,3) = self.backgroundColor(3);
                mask(:,:,4) = 255*aperture;
                self.maskTexture = Screen('MakeTexture', ...
                    self.windowNumber, ...
                    mask);
            end
        end
                
        function draw(self)
            
            % compute and draw
            for xx = 1:self.gridSize
                for yy = 1:self.gridSize
                    self.rdks(xx,yy).draw;
                end
            end
            
            if self.apertureFlag
                Screen('DrawTexture', ...
                    self.windowNumber, ...
                    self.maskTexture, ...
                    self.maskSourceRect, ...
                    self.maskDestinationRect);
            end
        end
    end
end